package com.sakura.demo2.gateway.authorization;

import cn.hutool.core.convert.Convert;
import com.sakura.demo.common.constant.RedisConstant;
import com.sakura.demo.common.model.UserInfoDto;
import com.sakura.demo.common.redis.RedisService;
import com.sakura.demo2.gateway.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: zhengcan
 * @Date: 2022/5/13
 * @Description: 鉴权管理器，用于判断是否有资源的访问权限
 * @Version: 1.0.0 创建
 */
@Slf4j
@Component
public class GatewayJwtTokenAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    @Autowired
    private RedisService redisService;

    /**
     * 要想在Mono<Authentication>中存在Authentication信息，必须要先进行token认证操作
     *  即ReactiveAuthenticationManager认证管理器要先authenticate()后，Mono<Authentication>中才有是否认证信息
     *      调用栈：
     *      1.如有filter在AuthenticationWebFilter之前的先执行；
     *      2.带token值过来时,先进AuthenticationWebFilter.authenticate()-->调默认的JwtReactiveAuthenticationManager.authenticate();
     *      3.认证通过后根据过滤器链往下走, 若未携带token信息，在跳过AuthenticationWebFilter，直接向后执行(关于filter执行顺序请查看SecurityWebFiltersOrder);
     *      4.执行到AuthorizationWebFilter.filter()-->先验证token是否为空：
     *          a.为空则不再进行权限校验，这也是白名单的原理；
     *          b.不为空则调用DelegatingReactiveAuthorizationManager.check()-->
     *                  若存在ReactiveAuthorizationManager<AuthorizationContext>类型的实现类，再调用其check()方法验证, 即本自定义实现类；
     *                  不存在则调用默认的AuthorityReactiveAuthorizationManager.check()或其他类型实现类处理
     *
     * @param mono
     * @param context
     * @return
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext context) {
        // 从请求头获取token信息，如果为空，则说明是白名单用户，不再进行鉴权操作, 与白名单过滤器配合使用
        String token = context.getExchange().getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (ObjectUtils.isEmpty(token)) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // 未找到什么原因，导致上下文中的token认证信息丢失，导致无法获取权限信息，造成鉴权失败，故换一种方法获取权限信息
//        Flux<String> map = ReactiveSecurityContextHolder.getContext()
//                .map(SecurityContext::getAuthentication)
//                .flatMapIterable(Authentication::getAuthorities)
//                .map(GrantedAuthority::getAuthority);
//        log.info("第一个权限值：{}",map.blockFirst());

        // 用户拥有的角色权限列表
        UserInfoDto userInfoDto = JwtTokenUtil.parseTokenToUserInfo(token);
        List<String> ownAuthorities = userInfoDto.getAuthorities();

        // 从redis中获取当前路径可访问角色列表
        URI uri = context.getExchange().getRequest().getURI();
        Object obj = redisService.hGet(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
        List<String> requestAuthorities = new ArrayList<>();
        if (null != obj) {
            requestAuthorities = Convert.toList(String.class, obj);
        }
        // 该路径没有权限则放过：1.放过，2.默认拦截，根据实际情况选择
        if (ObjectUtils.isEmpty(requestAuthorities)) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // 包含任一个权限则放行
        for (String authority : ownAuthorities) {
            if (requestAuthorities.contains(authority)) {
                return Mono.just(new AuthorizationDecision(true));
            }
        }
        return Mono.just(new AuthorizationDecision(false));
    }
}
